// Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// We'd like asprintf from stdio.h.
#define _GNU_SOURCE

#include <errno.h>
#include <dirent.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#define INTERVAL 15

#define TEMP_MIN 15
#define TEMP_MAX 140

#define THERM_MAX_DELTA 10

typedef enum {
	UNKNOWN,  // Failed to read the sensor data
	EXTREME,  // Temperature was outside the expected range
	OUTLIER,  // Temperature was too far from other readings
	GOOD      // As far as we know, temperature is correct
} sensor_health_t;

static const char* sensor_health_names[] = {
	"unknown", "extreme", "outlier", "good" };

typedef enum {
	CPU,
	THERM,
	NUM_SENSOR_TYPES
} sensor_type_t;

static struct sensor {
	const char* pathname;
	sensor_type_t type;
	sensor_health_t health;
	int temp;
	int fd;
	struct sensor* next;
}* sensors_head;

static const char* hwmon_path = "/sys/class/hwmon";

static const struct {
	int max_temp[NUM_SENSOR_TYPES];
	const char* max_freq;
} temp_map[] = {
	{ { 60, 49 }, "1700000" },
	{ { 61, 50 }, "1600000" },
	{ { 62, 51 }, "1500000" },
	{ { 63, 52 }, "1400000" },
	{ { 65, 55 }, "1300000" },
	{ { 67, 58 }, "1200000" },
	{ { 68, 60 }, "1100000" },
	{ { 69, 62 }, "1000000" },
	{ { 71, 64 },  "900000" },
	{ { 73, 65 },  "800000" },
};

#define TEMP_MAP_SIZE (sizeof(temp_map) / sizeof(temp_map[0]))

static const char* cpu_path_format =
	"/sys/devices/system/cpu/cpu%u/cpufreq/scaling_max_freq";

static struct cpu {
	unsigned index;
	int fd;
	struct cpu* next;
}* cpus_head;

#define BUFLEN 16

// Adds a discovered sensor to the list, taking ownership of the filename.
static struct sensor* add_sensor(const char* filename, sensor_type_t type,
                                 int fd, struct sensor* sensors_tail) {
	struct sensor* sensor = malloc(sizeof(struct sensor));
	if (!sensor) {
		syslog(LOG_ERR, "malloc failed!");
		exit(EXIT_FAILURE);
	}

	sensor->pathname = filename;
	sensor->type = type;
	sensor->health = UNKNOWN;
	sensor->temp = 0;
	sensor->fd = fd;
	sensor->next = NULL;

	if (!sensors_head)
		sensors_head = sensor;
	if (sensors_tail)
		sensors_tail->next = sensor;
	return sensor;
}

static void find_sensors(void) {
	DIR* dir;
	struct dirent* entry;
	struct sensor* sensors_tail;

	dir = opendir(hwmon_path);
	if (!dir) {
		syslog(LOG_ERR, "Couldn't opendir %s: %s", hwmon_path,
		       strerror(errno));
		exit(EXIT_FAILURE);
	}

	sensors_tail = NULL;
	while ((entry = readdir(dir))) {
		char* filename;
		char* pathname;
		int fd;
		int n;

		filename = entry->d_name;
		if (!strcmp(filename, ".") || !strcmp(filename, ".."))
			continue;

		if (asprintf(&pathname, "%s/%s/temp1_input",
			     hwmon_path, filename) < 2) {
			syslog(LOG_ERR, "asprintf failed!");
			exit(EXIT_FAILURE);
		}

		fd = open(pathname, O_RDONLY);
		if (fd >= 0) {
			syslog(LOG_INFO, "Found CPU sensor at %s.\n",
			       filename);
			sensors_tail = add_sensor(pathname, CPU, fd,
			                          sensors_tail);
			continue;
		} else {
			free(pathname);
		}

		for (n = 1; ; n++) {
			if (asprintf(&pathname, "%s/%s/device/temp%d_input",
			             hwmon_path, filename, n) < 2) {
				syslog(LOG_ERR, "asprintf failed!");
				exit(EXIT_FAILURE);
			}

			fd = open(pathname, O_RDONLY);
			if (fd < 0)
				break;

			syslog(LOG_INFO, "Found thermistor at %s/%d.\n",
			       filename, n);
			sensors_tail = add_sensor(pathname, THERM, fd,
                                                  sensors_tail);
		}
		free(pathname);

		if (n == 1) {
			syslog(LOG_WARNING,
			       "No temperature sensor found at %s.",
			       filename);
		}
	}

	if (!sensors_head) {
		syslog(LOG_ERR, "No sensors found; exiting.");
		exit(EXIT_FAILURE);
	}
}

static void read_sensor(struct sensor* sensor) {
	char buf[16];
	char* endptr;
	int rv;
	long raw;

	rv = pread(sensor->fd, buf, sizeof(buf), 0);
	if (rv < 1) {
		syslog(LOG_WARNING, "Couldn't read sensor %s: %s",
		       sensor->pathname, strerror(errno));
		sensor->health = UNKNOWN;
		return;
	}

	raw = strtol(buf, &endptr, 10);
	if (*endptr != '\0' && *endptr != '\n') {
		syslog(LOG_WARNING, "Couldn't parse sensor %s value",
		       sensor->pathname);
		sensor->health = UNKNOWN;
		return;
	}

	sensor->temp = raw / 1000;
	if (sensor->temp >= TEMP_MIN && sensor->temp <= TEMP_MAX)
		sensor->health = GOOD;
	else
		sensor->health = EXTREME;
}

static void read_sensors(void) {
	struct sensor* sensor;

	for (sensor = sensors_head; sensor; sensor = sensor->next) {
		read_sensor(sensor);
	}
}

static struct cpu *add_cpu(unsigned index, int fd,
                           struct cpu* cpus_tail) {
	struct cpu* cpu;

	cpu = malloc(sizeof(*cpu));
	if (!cpu) {
		syslog(LOG_ERR, "malloc failed!");
		exit(EXIT_FAILURE);
	}

	cpu->index = index;
	cpu->fd = fd;
	cpu->next = NULL;

	if (!cpus_head)
		cpus_head = cpu;
	if (cpus_tail)
		cpus_tail->next = cpu;
	return cpu;
}

static void find_cpus() {
	struct cpu* cpus_tail;
	unsigned i;
	unsigned count;
	char* filename;
	int fd;

	cpus_tail = NULL;
	count = 0;
	for (i = 0; ; i++) {
		if (asprintf(&filename, cpu_path_format, i) < 1) {
			syslog(LOG_ERR, "asprintf failed!");
			exit(EXIT_FAILURE);
		}

		fd = open(filename, O_WRONLY);
		if (fd < 0) {
			syslog(LOG_INFO, "No cpu %u.", i);
			break;
		}

		syslog(LOG_INFO, "Found cpu %u.", i);

		cpus_tail = add_cpu(i, fd, cpus_tail);
		count++;
	}

	if (count == 0) {
		syslog(LOG_ERR, "No cpus found!");
		exit(EXIT_FAILURE);
	}

	syslog(LOG_INFO, "Found %u CPU(s).", count);
}

static void write_cpu(struct cpu* cpu, const char* max_freq, int len) {
	int rv;

	rv = pwrite(cpu->fd, max_freq, len, 0);
	if (rv < 0) {
		syslog(LOG_ERR, "Couldn't write cpu %u: %s", cpu->index,
		       strerror(errno));
		exit(EXIT_FAILURE);
	}
}

static void write_cpus(const char* max_freq) {
	struct cpu* cpu;
	int len = strlen(max_freq);

	syslog(LOG_NOTICE, "Setting CPU frequencies to %s kHz", max_freq);

	for (cpu = cpus_head; cpu; cpu = cpu->next) {
		write_cpu(cpu, max_freq, len);
	}
}

static void cross_check_thermistors(void) {
	struct sensor* sensor_i;
	struct sensor* sensor_j;
	int delta;
	int is_outlier;

	for (sensor_i = sensors_head; sensor_i; sensor_i = sensor_i->next) {
		if (sensor_i->type != THERM || sensor_i->health != GOOD)
			continue;

		is_outlier = 1;

		for (sensor_j = sensors_head;
		     sensor_j;
		     sensor_j = sensor_j->next) {
			if (sensor_j->type != THERM
			    || (sensor_j->health != GOOD &&
			        sensor_j->health != OUTLIER))
				continue;

			delta = abs(sensor_i->temp - sensor_j->temp);
			if (delta < THERM_MAX_DELTA) {
				is_outlier = 0;
			}
		}

		if (is_outlier) {
			sensor_i->health = OUTLIER;
		}
	}
}

static void update_index_from_sensor(struct sensor* sensor, int* index) {
	int sensor_index = -1;
	unsigned i;

	if (sensor->health != GOOD)
		return;

	for (i = 0; i < TEMP_MAP_SIZE; i++) {
		if (sensor->temp <= temp_map[i].max_temp[sensor->type]) {
			sensor_index = i;
			break;
		}
	}

	if (sensor_index == -1) {
		syslog(LOG_WARNING, "Temp %d from sensor %s not in temp map!",
		       sensor->temp, sensor->pathname);
		sensor_index = TEMP_MAP_SIZE - 1;
	}

	if (sensor_index > *index) {
		*index = sensor_index;
	}
}

static void find_map_entry(int* index_out) {
	struct sensor* sensor;

	*index_out = -1;

	for (sensor = sensors_head; sensor; sensor = sensor->next)
		update_index_from_sensor(sensor, index_out);

	if (*index_out == -1)
		syslog(LOG_ERR, "Flying blind!  No sensors readable?");
}

static void log_sensors(void) {
	struct sensor* sensor;

	for (sensor = sensors_head; sensor; sensor = sensor->next) {
		syslog(LOG_INFO, "Sensor %s: %d (%s)",
		       sensor->pathname,
		       sensor->temp,
		       sensor_health_names[sensor->health]);
	}
}

int main(int argc, char** argv) {
	int last_index = -1;
	int index;

	if (argc > 1) {
		fprintf(stderr, "Usage: %s\n", argv[0]);
		exit(EXIT_FAILURE);
	}

	openlog(argv[0], LOG_PERROR, LOG_DAEMON);
	find_sensors();
	find_cpus();

	while (1) {
		read_sensors();
		cross_check_thermistors();
		find_map_entry(&index);

		if (index != last_index) {
			log_sensors();
			if (index != -1) {
				write_cpus(temp_map[index].max_freq);
				last_index = index;
			}
		}
		sleep(INTERVAL);
	}
}
